<?php
class CV_Collection extends ArrayObject{
	/**
	 * @return CV_Collection
	 */
	public function mmap( $method ){
		$return = array();
		foreach( $this as $key => $item ){
			$return[$key] = $item->$method(); 
		}
		return new CV_Collection( $return );
	}
	/**
	 * experimental
	 */
	public function reduce_( $callback, $initial = NULL ){
		if( is_int($initial) ) {
			return array_reduce( $this->getArrayCopy(), $callback, $initial );
		} else {
			$array = $this->getArrayCopy();
			if( $initial === NULL ){
				$current = array_shift( $array );
			} else {
				$current = $initial;
			}
			foreach( $array as $item ){
				$current = call_user_func( $callback, $current, $item );
			}
			return $current;
		}
	}
	/**
	 * experimental
	 */
	public function mreduce_( $method, $initial = NULL ){
		$array = $this->getArrayCopy();
		if( $initial === NULL ){
			$current = array_shift( $array );
		} else {
			$current = $initial;
		}
		foreach( $array as $item ){
			$current->$method( $item );
		}
		return $current;
	}
	public function first(){
		$array = $this->getArrayCopy();
		reset($array);
		return current($array);
	}
	/**
	 * @return CV_Collection
	 */
	public function column( $name ){
		return new CV_Collection( CV_Array::column($this->getArrayCopy(), $name) );
	}
	public function max(){
		return max( $this->getArrayCopy() );
	}
	public function sum(){
		return array_sum( $this->getArrayCopy() );
	}
	/**
	 * @return CV_Collection
	 */
	public function reindex( $key = NULL ){
		if( $key === NULL ){
			$this->exchangeArray( array_values($this->getArrayCopy()) );
		} else {
			$this->exchangeArray( CV_Array::reindex($this->getArrayCopy(), $key) );
		}		
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function map( $callback ){
		return new CV_Collection( array_map($callback, $this->getArrayCopy()) );
	}
	public function implode( $separator ){
		return implode( $separator, $this->getArrayCopy() );
	}
	/**
	 * @return CV_Collection
	 */
	public function copy(){
		return clone $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function prepend( $item ){
		$array = $this->getArrayCopy();
		array_unshift( $array, $item );
		$this->exchangeArray( $array );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function intersect( $array ){
		$array = self::unwrap($array);
		$this->exchangeArray( array_uintersect($this->getArrayCopy(), $array, array($this,'_diff_cmp')) );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function diff( $array ){
		$array = self::unwrap($array);
		$this->exchangeArray( array_udiff($this->getArrayCopy(), $array, array($this,'_diff_cmp')) );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function rdiff( $collection ){
		$collection = self::wrap( $collection );
		return $collection->copy()->diff( $this );
	}
	protected function _diff_cmp($a, $b){
        if ($a === $b) return 0;
        elseif( $a < $b ) return -1;
        else return 1;
	}
	/**
	 * converts and instance of CV_Collection to array, leaves alone everything else
	 */
	public static function unwrap( $array ){
		if( $array instanceof CV_Collection ){
			$array = $array->getArrayCopy();
		}
		return $array;
	}
	public static function wrap( $array ){
		if( $array instanceof CV_Collection ){
			return $array;
		}
		return new CV_Collection( $array );
	}
	public static function unwrap_recursive( $array ){
		if( $array instanceof CV_Collection ){
			$array = $array->getArrayCopy();
		}
		if( is_array($array) ){
			$array = array_map( array('self','unwrap_recursive'), $array );
		}
		return $array;
	}
	public function unique(){
		$this->exchangeArray( array_unique($this->getArrayCopy()) );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function comprehend( $expression, $variables = array() ){
		$split_expression = explode( "=>", $expression);
		if( count($split_expression) === 1 ){
			$value_expression = 'return '.$split_expression[0].";";
		} else {
			$key_expression = 'return '.$split_expression[0].";";
			$value_expression = 'return '.$split_expression[1].";";
		}
		
		$return = new CV_Collection;
		extract( $variables );
		foreach( $this as $k => $v ){
			if( isset($key_expression) ){
				$return[ eval($key_expression) ] = eval( $value_expression );
			} else {
				$return[] = eval( $value_expression );
			}
		}
		return $return;
	}
	public function contains( $value, $strict = true ){
		return in_array( $value, $this->getArrayCopy(), $strict );
	}
	public function search( $value, $strict = true ){
		return array_search( $value, $this->getArrayCopy(), $strict );
	}
	/**
	 * @return CV_Collection
	 */
	public function where( $expression, $vars = array() ){
		foreach( $this->getArrayCopy() as $key => $value ){
			if( !$this->_evaluate_expression($expression,$value,$key,$vars) ){
				unset($this[$key]);
			}
		}
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function values(){
		return new CV_Collection( array_values($this->getArrayCopy()) );
	}
	/**
	 * @return CV_Collection
	 */
	public function keys(){
		return new CV_Collection( array_keys($this->getArrayCopy()) );
	}
	protected function _evaluate_expression( $_expression, $v, $k, $vars = array() ){
		$value = $v;
		$key = $k;
		if( is_array($v) ){
			extract($v);
		} elseif( $v instanceof ArrayObject ) {
			extract( $v->getArrayCopy() );
		} elseif( is_object($v) ) {
			extract( get_object_vars($v) );
		}
		extract( $vars );
		return eval("return $_expression;");
	}
	/**
	 * @return CV_Collection
	 */
	public function group_by( $column ){
		$class = get_class( $this );
		$return = new CV_Collection();
		foreach( $this as $key => $value ){
			if( !isset($return[$value[$column]]) ){
				$return[$value[$column]] = new $class();
			}
			$return[$value[$column]][$key] = $value;
		}
		return $return;
	}
	/**
	 * @return CV_Collection
	 */
	public function merge( $array ){
		$array = CV_Collection::unwrap($array);
		$this->exchangeArray( array_merge($this->getArrayCopy(), $array) );
		return $this;
	}
	public function merge_all(){
		return $this->mreduce_( 'merge' );
	}
	/**
	 * @return CV_Collection
	 */
	public function reverse(){
		$this->exchangeArray( array_reverse($this->getArrayCopy(), true) );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function asort( $flags = NULL ){
		if( $flags !== NULL ){
			$array = $this->getArrayCopy();
			asort( $array, $flags );
			$this->exchangeArray( $array );
		} else {
			parent::asort();
		}		
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function ksort( $flags = NULL ){
		if( $flags !== NULL ){
			$array = $this->getArrayCopy();
			ksort( $array, $flags );
			$this->exchangeArray( $array );
		} else {
			parent::ksort();
		}
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function uasort( $cmp_function ){
		parent::uasort( $cmp_function );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function uksort( $cmp_function ){
		parent::uksort( $cmp_function );
		return $this;
	}
	/**
	 * @return CV_Collection
	 */
	public function lc( $expression, $vars = array() ){
		$args = array_merge( array('this'=>$this->getArrayCopy()), $vars );
		return new CV_Collection( ListComprehension::execute($expression, $args ) ) ;
	}
	/**
	 * @return CV_Collection
	 */
	public function multisort(){
		$refl = new ReflectionFunction( 'array_multisort' );
		$args = func_get_args();
		$m_args = array();
		foreach( $args as $key => $arg ){
			$args[$key] = CV_Collection::unwrap( $arg );
			$m_args[] = &$args[$key];
		}
		
		$results = $this->getArrayCopy();
		$m_args[] = &$results;
		
		$refl->invokeArgs( $m_args );
		$this->exchangeArray( $results );
		return $this;
	}
	/**
	 * This is a temporary implementation.
	 * @return CV_Collection
	 */
	public function columnsort(){
		$args = func_get_args();
		CV_Array::$_columnsort_order = $args;
		$this->uasort( array('CV_Array', '_columnsort_cmp') );
		return $this;
	}
	/**
	 * sort a 2 dimensional array by columns
	 * improvement ideas:
	 * 	- default sorting order (ASC OR DESC) as first parameter
	 *  - state sort flag. currently SORT_LOCALE_STRING is used all the time. could be given as in multisort.
	 * 
	 * @return CV_Collection
	 */
	public function _columnsort(){
		$args = func_get_args();
		foreach( $args as $key => $arg ){
			if( !in_array($arg, array(SORT_ASC,SORT_DESC)) ){
				// replace column name by sort key array for multisort.
				// multisort does not respected the locale setting
				// so in order to sort correctly this sorts using asort
				// and creates a numeric array to be used by multisort
				// to sort correctly respecting the locale
				$args[$key] = 
					$this->column( $arg )
					->asort( SORT_LOCALE_STRING )
					->keys()
					->asort( SORT_NUMERIC )
					->keys(); 
			}
		}
		print_r($args);
		$refl = new ReflectionMethod( 'CV_Collection', 'multisort' );
		$refl->invokeArgs( $this, $args );
		return $this;
	}
}